使用 Spring Boot 開發程式真的很快,透過 Lombok 不用寫 Getter / Setter / Constructor,現在連記憶儲存的類別都不讓我們動手,在不努力點就真的要被 AI 取代了XD
Spring AI 從 1.0.0 開始提供了 ChatMemory 介面,目前有一個 InMemoryChatMemory 實作類別,下面是原始程式碼,除了記錄歷史訊息外,透過 ChatID 還能區分不同對話
public class InMemoryChatMemory implements ChatMemory {
Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
@Override
public void add(String conversationId, List<Message> messages) {
this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
this.conversationHistory.get(conversationId).addAll(messages);
}
@Override
public List<Message> get(String conversationId, int lastN) {
List<Message> all = this.conversationHistory.get(conversationId);
return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
}
@Override
public void clear(String conversationId) {
this.conversationHistory.remove(conversationId);
}
}
研究其程式碼可發現內部使用 ConcurrentHashMap<String, List<Message>>
儲存資料,每個 Chat ID 就能有獨立的記憶,而 ConcurrentHashMap 有 線程安全 跟 無阻塞存取 的特性,非常適合多人同時使用
取得資料的 get 方法除了 ChatID 外還多了 lastN 參數,透過 stream 的 skip 方法可篩選出最近 lastN 筆資料
最後還提供了 clear 方法可用來清除某個 ChatID 資料
接下來開始改寫昨天的程式,將土炮記憶改為原裝的 InMemoryChatMemory
因為之後程式開始複雜,凱文大叔將複雜的邏輯獨立到 Service 元件處理
@RequiredArgsConstructor
@Service
public class ChatService {
private final ChatClient chatClient;
private ChatMemory chatMemory = new InMemoryChatMemory();
public String chat(String chatId, String userMessage) {
chatMemory.add(chatId, new UserMessage(userMessage)); //存對話時多了chatId
return this.chatClient.prompt()
.messages(chatMemory.get(chatId, 30)) //取對話時多了chatId與筆數
.call()
.content();
}
}
原本的 Controller 專心處理轉派
@RestController
@RequiredArgsConstructor
public class AiClientController {
private final ChatService chatService;
@GetMapping("/memchat")
public String chat(@RequestParam String chatId, @RequestParam String prompt) {
return chatService.chat(chatId, prompt);
}
}
接著測試兩個主要功能
ChatID = 2
提問1: 我是凱文大叔
提問2: 我是誰
ChatID = 2
提問3: 我是誰
測試結果可以看出在 ChatID = 1 時,能記住我是凱文大叔,一但切到 ChatID = 2 又是另一段記憶
這次將記憶數量改成2 .messages(chatMemory.get(chatId, 2))
,確認第三次發問時能否有之前的記憶
提問1: 我是凱文大叔
提問2: 金魚的記憶有幾秒?
提問3: 還記得我是誰嗎?
記憶上限的參數也有作用
通常歷史訊息的數量可設為 20~30 左右,超過的話除了對 AI 的回答沒幫助外,也容易超過每次傳訊的 Token 限制,當然越多 Token 花的錢也越多,有效控制歷史訊息數量就非常重要了
今日學到甚麼?
程式碼下載: https://github.com/kevintsai1202/SpringBoot-AI-Day18.git
凱文大叔使用 Java 開發程式超過 20 年,對於 Java 生態非常熟悉,曾使用反射機制開發 ETL 框架,對 Spring 背後的原理非常清楚,目前以 Spring Boot 作為後端開發框架,前端使用 React 搭配 Ant Design
下班之餘在 Amazing Talker 擔任程式語言講師,並獲得學員的一致好評
最近剛成立一個粉絲專頁-凱文大叔教你寫程式 歡迎大家多追蹤,我會不定期分享實用的知識以及程式開發技巧
想討論 Spring 的 Java 開發人員可以加入 FB 討論區 Spring Boot Developer Taiwan
我是凱文大叔,歡迎一起加入學習程式的行列